/*
 * Copyright 2017 Google Inc. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.github.webbluetoothcg.bletestperipheral;

import android.app.Activity;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothGattService;
import android.os.Bundle;
import android.os.ParcelUuid;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.EditorInfo;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.TextView.OnEditorActionListener;
import android.widget.Toast;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Timer;
import java.util.TimerTask;
import java.util.UUID;

public class HealthThermometerServiceFragment extends ServiceFragment {
	/**
	 * See <a href="https://developer.bluetooth.org/gatt/services/Pages/ServiceViewer.aspx?u=org.bluetooth.service.health_thermometer.xml">
	 * Health Thermometer Service</a>
	 * This service exposes two characteristics with descriptors:
	 * - Measurement Interval Characteristic:
	 * - Listen to notifications to from which you can subscribe to notifications
	 * - CCCD Descriptor:
	 * - Read/Write to get/set notifications.
	 * - User Description Descriptor:
	 * - Read/Write to get/set the description of the Characteristic.
	 * - Temperature Measurement Characteristic:
	 * - Read value to get the current interval of the temperature measurement timer.
	 * - Write value resets the temperature measurement timer with the new value. This timer
	 * is responsible for triggering value changed events every "Measurement Interval" value.
	 * - CCCD Descriptor:
	 * - Read/Write to get/set notifications.
	 * - User Description Descriptor:
	 * - Read/Write to get/set the description of the Characteristic.
	 */
	private static final UUID HEALTH_THERMOMETER_SERVICE_UUID = UUID
			.fromString("00001809-0000-1000-8000-00805f9b34fb");

	/**
	 * See <a href="https://developer.bluetooth.org/gatt/characteristics/Pages/CharacteristicViewer.aspx?u=org.bluetooth.characteristic.temperature_measurement.xml">
	 * Temperature Measurement</a>
	 */
	private static final UUID TEMPERATURE_MEASUREMENT_UUID = UUID
			.fromString("00002A1C-0000-1000-8000-00805f9b34fb");
	private static final int TEMPERATURE_MEASUREMENT_VALUE_FORMAT = BluetoothGattCharacteristic.FORMAT_FLOAT;
	private static final float INITIAL_TEMPERATURE_MEASUREMENT_VALUE = 37.0f;
	private static final int EXPONENT_MASK = 0x7f800000;
	private static final int EXPONENT_SHIFT = 23;
	private static final int MANTISSA_MASK = 0x007fffff;
	private static final int MANTISSA_SHIFT = 0;
	private static final String TEMPERATURE_MEASUREMENT_DESCRIPTION = "This characteristic is used " +
			"to send a temperature measurement.";

	/**
	 * See <a href="https://developer.bluetooth.org/gatt/characteristics/Pages/CharacteristicViewer.aspx?u=org.bluetooth.characteristic.measurement_interval.xml">
	 * Measurement Interval</a>
	 */
	private static final UUID MEASUREMENT_INTERVAL_UUID = UUID
			.fromString("00002A21-0000-1000-8000-00805f9b34fb");
	private static final int MEASUREMENT_INTERVAL_FORMAT = BluetoothGattCharacteristic.FORMAT_UINT16;
	private static final int INITIAL_MEASUREMENT_INTERVAL = 1;
	private static final int MIN_MEASUREMENT_INTERVAL = 1;
	private static final int MAX_MEASUREMENT_INTERVAL = (int) Math.pow(2, 16) - 1;
	private static final String MEASUREMENT_INTERVAL_DESCRIPTION = "This characteristic is used " +
			"to enable and control the interval between consecutive temperature measurements.";

	private BluetoothGattService mHealthThermometerService;
	private BluetoothGattCharacteristic mTemperatureMeasurementCharacteristic;
	private BluetoothGattCharacteristic mMeasurementIntervalCharacteristic;
	private BluetoothGattDescriptor mMeasurementIntervalCCCDescriptor;

	private ServiceFragmentDelegate mDelegate;

	private Timer mTimer;

	private EditText mEditTextTemperatureMeasurement;
	private final OnEditorActionListener mOnEditorActionListenerTemperatureMeasurement = new OnEditorActionListener() {
		@Override
		public boolean onEditorAction(TextView textView, int actionId, KeyEvent event) {
			if (actionId == EditorInfo.IME_ACTION_DONE) {
				String newTemperatureMeasurementValueString = textView.getText().toString();
				if (isValidTemperatureMeasurementValue(newTemperatureMeasurementValueString)) {
					float newTemperatureMeasurementValue = Float.valueOf(newTemperatureMeasurementValueString);
					setTemperatureMeasurementValue(newTemperatureMeasurementValue);
				} else {
					Toast.makeText(getActivity(), R.string.temperatureMeasurementValueInvalid,
							Toast.LENGTH_SHORT).show();
				}
			}
			return false;
		}
	};

	private final OnEditorActionListener mOnEditorActionListenerMeasurementInterval = new OnEditorActionListener() {
		@Override
		public boolean onEditorAction(TextView textView, int actionId, KeyEvent event) {
			if (actionId == EditorInfo.IME_ACTION_DONE) {
				int newMeasurementInterval = Integer.parseInt(textView.getText().toString());
				if (isValidMeasurementIntervalValue(newMeasurementInterval)) {
					mMeasurementIntervalCharacteristic.setValue(newMeasurementInterval,
							MEASUREMENT_INTERVAL_FORMAT,
							/* offset */ 0);
					resetTimer(newMeasurementInterval);
				} else {
					Toast.makeText(getActivity(), R.string.measurementIntervalInvalid,
							Toast.LENGTH_SHORT).show();
				}
			}
			return false;
		}
	};
	private EditText mEditTextMeasurementInterval;

	private TextView mTextViewNotifications;

	public HealthThermometerServiceFragment() {
		mTemperatureMeasurementCharacteristic =
				new BluetoothGattCharacteristic(TEMPERATURE_MEASUREMENT_UUID,
						BluetoothGattCharacteristic.PROPERTY_INDICATE,
						/* No permissions */ 0);

		mTemperatureMeasurementCharacteristic.addDescriptor(
				Peripheral.getClientCharacteristicConfigurationDescriptor());

		mTemperatureMeasurementCharacteristic.addDescriptor(
				Peripheral.getCharacteristicUserDescriptionDescriptor(TEMPERATURE_MEASUREMENT_DESCRIPTION));

		mMeasurementIntervalCharacteristic =
				new BluetoothGattCharacteristic(
						MEASUREMENT_INTERVAL_UUID,
						(BluetoothGattCharacteristic.PROPERTY_READ |
								BluetoothGattCharacteristic.PROPERTY_WRITE |
								BluetoothGattCharacteristic.PROPERTY_INDICATE),
						(BluetoothGattCharacteristic.PERMISSION_READ |
								BluetoothGattCharacteristic.PERMISSION_WRITE));

		mMeasurementIntervalCCCDescriptor = Peripheral.getClientCharacteristicConfigurationDescriptor();
		mMeasurementIntervalCharacteristic.addDescriptor(mMeasurementIntervalCCCDescriptor);

		mMeasurementIntervalCharacteristic.addDescriptor(
				Peripheral.getCharacteristicUserDescriptionDescriptor(MEASUREMENT_INTERVAL_DESCRIPTION));

		mHealthThermometerService = new BluetoothGattService(HEALTH_THERMOMETER_SERVICE_UUID,
				BluetoothGattService.SERVICE_TYPE_PRIMARY);
		mHealthThermometerService.addCharacteristic(mTemperatureMeasurementCharacteristic);
		mHealthThermometerService.addCharacteristic(mMeasurementIntervalCharacteristic);
	}


	@Override
	public View onCreateView(LayoutInflater inflater, ViewGroup container,
							 Bundle savedInstanceState) {

		View view = inflater.inflate(R.layout.fragment_health_thermometer, container, false);
		mEditTextTemperatureMeasurement = (EditText) view
				.findViewById(R.id.editText_temperatureMeasurementValue);
		mEditTextTemperatureMeasurement
				.setOnEditorActionListener(mOnEditorActionListenerTemperatureMeasurement);
		mEditTextMeasurementInterval = (EditText) view
				.findViewById(R.id.editText_measurementIntervalValue);
		mEditTextMeasurementInterval
				.setOnEditorActionListener(mOnEditorActionListenerMeasurementInterval);

		mEditTextTemperatureMeasurement.setText(Float.toString(INITIAL_TEMPERATURE_MEASUREMENT_VALUE));
		setTemperatureMeasurementValue(INITIAL_TEMPERATURE_MEASUREMENT_VALUE);

		mMeasurementIntervalCharacteristic.setValue(INITIAL_MEASUREMENT_INTERVAL,
				MEASUREMENT_INTERVAL_FORMAT,
				/* offset */ 0);
		mEditTextMeasurementInterval.setText(Integer.toString(INITIAL_MEASUREMENT_INTERVAL));

		mTextViewNotifications = (TextView) view.findViewById(R.id.textView_notifications);
		mTextViewNotifications.setText(R.string.notificationsNotEnabled);

		return view;
	}

	@Override
	public void onAttach(Activity activity) {
		super.onAttach(activity);
		try {
			mDelegate = (ServiceFragmentDelegate) activity;
		} catch (ClassCastException e) {
			throw new ClassCastException(activity.toString()
					+ " must implement ServiceFragmentDelegate");
		}
	}

	@Override
	public void onDetach() {
		super.onDetach();
		mDelegate = null;
	}

	@Override
	public void onStop() {
		super.onStop();
		cancelTimer();
	}

	@Override
	public BluetoothGattService getBluetoothGattService() {
		return mHealthThermometerService;
	}

	@Override
	public ParcelUuid getServiceUUID() {
		return new ParcelUuid(HEALTH_THERMOMETER_SERVICE_UUID);
	}

	private void setTemperatureMeasurementValue(float temperatureMeasurementValue) {

		/* Set the org.bluetooth.characteristic.temperature_measurement
		 * characteristic to a byte array of size 5 so
		 * we can use setValue(value, format, offset);
		 *
		 * Flags (8bit) + Temperature Measurement Value (float) = 5 bytes
		 *
		 * Flags:
		 *   Temperature Units Flag (0) -> Celsius
		 *   Time Stamp Flag (0) -> Time Stamp field not present
		 *   Temperature Type Flag (0) -> Temperature Type field not present
		 *   Unused (00000)
		 */
		mTemperatureMeasurementCharacteristic.setValue(new byte[]{0b00000000, 0, 0, 0, 0});
		// Characteristic Value: [flags, 0, 0, 0, 0]

		int bits = Float.floatToIntBits(temperatureMeasurementValue);
		int exponent = (bits & EXPONENT_MASK) >>> EXPONENT_SHIFT;
		int mantissa = (bits & MANTISSA_MASK) >>> MANTISSA_SHIFT;

		mTemperatureMeasurementCharacteristic.setValue(mantissa, exponent,
				TEMPERATURE_MEASUREMENT_VALUE_FORMAT,
				/* offset */ 1);
		// Characteristic Value: [flags, temperature measurement value]
	}

	private void setTemperatureMeasurementTimerInterval(int measurementIntervalValueSeconds) {
		mTimer = new Timer();
		mTimer.scheduleAtFixedRate(new TimerTask() {
			@Override
			public void run() {
				getActivity().runOnUiThread(new Runnable() {
					@Override
					public void run() {
						mDelegate.sendNotificationToDevices(mTemperatureMeasurementCharacteristic);
					}
				});
			}
		}, 0 /* delay */, measurementIntervalValueSeconds * 1000);
	}

	private void cancelTimer() {
		if (mTimer != null) {
			mTimer.cancel();
		}
	}

	private void resetTimer(int measurementIntervalValue) {
		cancelTimer();
		setTemperatureMeasurementTimerInterval(measurementIntervalValue);
	}

	private boolean isValidTemperatureMeasurementValue(String s) {
		try {
			float value = Float.valueOf(s);
			return true;
		} catch (NumberFormatException e) {
			return false;
		}
	}

	private boolean isValidMeasurementIntervalValue(int value) {
		return (value >= MIN_MEASUREMENT_INTERVAL) && (value <= MAX_MEASUREMENT_INTERVAL);
	}

	@Override
	public int writeCharacteristic(BluetoothGattCharacteristic characteristic, int offset, byte[] value) {
		if (offset != 0) {
			return BluetoothGatt.GATT_INVALID_OFFSET;
		}
		// Measurement Interval is a 16bit characteristic
		if (value.length != 2) {
			return BluetoothGatt.GATT_INVALID_ATTRIBUTE_LENGTH;
		}
		ByteBuffer byteBuffer = ByteBuffer.wrap(value);
		byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
		final int newMeasurementIntervalValue = byteBuffer.getShort();
		if (!isValidMeasurementIntervalValue(newMeasurementIntervalValue)) {
			return BluetoothGatt.GATT_FAILURE;
		}
		getActivity().runOnUiThread(new Runnable() {
			@Override
			public void run() {
				mMeasurementIntervalCharacteristic.setValue(newMeasurementIntervalValue,
						MEASUREMENT_INTERVAL_FORMAT,
						/* offset */ 0);
				if (mMeasurementIntervalCCCDescriptor.getValue() == BluetoothGattDescriptor.ENABLE_INDICATION_VALUE) {
					resetTimer(newMeasurementIntervalValue);
					mTextViewNotifications.setText(R.string.notificationsEnabled);
				}
			}
		});
		return BluetoothGatt.GATT_SUCCESS;
	}

	@Override
	public void notificationsDisabled(BluetoothGattCharacteristic characteristic) {
		if (characteristic.getUuid() != TEMPERATURE_MEASUREMENT_UUID) {
			return;
		}
		cancelTimer();
		getActivity().runOnUiThread(new Runnable() {
			@Override
			public void run() {
				mTextViewNotifications.setText(R.string.notificationsNotEnabled);
			}
		});
	}

	@Override
	public void notificationsEnabled(BluetoothGattCharacteristic characteristic, boolean indicate) {
		if (characteristic.getUuid() != TEMPERATURE_MEASUREMENT_UUID) {
			return;
		}
		if (!indicate) {
			return;
		}
		getActivity().runOnUiThread(new Runnable() {
			@Override
			public void run() {
				int newMeasurementInterval = Integer.parseInt(mEditTextMeasurementInterval.getText()
						.toString());
				if (isValidMeasurementIntervalValue(newMeasurementInterval)) {
					mMeasurementIntervalCharacteristic.setValue(newMeasurementInterval,
							MEASUREMENT_INTERVAL_FORMAT,
							/* offset */ 0);
					resetTimer(newMeasurementInterval);
					mTextViewNotifications.setText(R.string.notificationsEnabled);
				}
			}
		});
	}

}
